﻿using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Xml;
using System.Linq;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Globalization;
using System.IO;

namespace SceneEngine
{
    public class Scene
    {
        private Dictionary<string, BaseObject> sceneObjects = 
            new Dictionary<string, BaseObject>();

        private HeightMap heightMap;

        private string heightMapPath;
        private string colorMapPath;

        public HeightMap HeightMap
        {
            get { return heightMap; }
            set { heightMap = value; }
        }

        public string HeightMapPath
        {
            get { return heightMapPath; }
            set { heightMapPath = value; }
        }
        public string ColorMapPath
        {
            get { return colorMapPath; }
            set { colorMapPath = value; }
        }

        /// <summary>
        /// Returns a list of the names of the scenes objects.
        /// </summary>
        public List<string> ObjectNames
        {
            get { return sceneObjects.Keys.ToList(); }
        }
        /// <summary>
        /// Returns a list of the baseObjects that exist in the scene.
        /// </summary>
        public List<BaseObject> SceneObjects
        {
            get { return sceneObjects.Values.ToList(); }
        }

        /// <summary>
        /// An event that fires when an object is renamed.
        /// </summary>
        public event RenamedEventHandler ObjectRenamed;

        /// <summary>
        /// Private constructor for the content pipeline.
        /// </summary>
        public Scene()
        {
            EngineGlobals.Scene = this;
        }

        /// <summary>
        /// Invokes the ObjectRenamed event.
        /// </summary>
        private void OnRenamed(RenamedEventArgs e)
        {
            if (ObjectRenamed != null)
                ObjectRenamed(this, e);
        }

        /// <summary>
        /// The method called when an entity is renamed.
        /// </summary>
        private void BaseObjectRenamed(object sender, RenamedEventArgs e)
        {
            BaseObject entity = sceneObjects[e.PreviousName];

            sceneObjects.Remove(e.PreviousName);
            sceneObjects.Add(e.NewName, entity);
        }

        /// <summary>
        /// Outputs a list of objects to an Xml writer.
        /// </summary>
        private void WriteBaseObjects(XmlTextWriter writer)
        {
            writer.WriteStartElement("SceneObjects");

            foreach (string entityName in ObjectNames)
            {
                BaseObject entity = GetBaseObject(entityName);

                writer.WriteStartElement("SceneObject");

                writer.WriteAttributeString("Name", entityName);
                writer.WriteAttributeString("AssetPath", entity.ModelPath);
                writer.WriteAttributeString("GroupPath", entity.GroupPath);

                string position = string.Format(CultureInfo.InvariantCulture, 
                                                "{0} {1} {2}", 
                                                entity.Position.X,
                                                entity.Position.Y, 
                                                entity.Position.Z);

                writer.WriteAttributeString("Position", position);


                string rotation = string.Format(CultureInfo.InvariantCulture,
                                                "{0} {1} {2} {3}", 
                                                entity.Rotation.X,
                                                entity.Rotation.Y, 
                                                entity.Rotation.Z,
                                                entity.Rotation.W);

                writer.WriteAttributeString("Rotation", rotation);


                string scale = string.Format(CultureInfo.InvariantCulture, 
                                             "{0} {1} {2}", 
                                             entity.Scale.X,
                                             entity.Scale.Y, 
                                             entity.Scale.Z);

                writer.WriteAttributeString("Scale", scale);

                //writer.WriteAttributeString("Visible", "true");

                writer.WriteAttributeString("PlaceOnTerrain", entity.PlaceOnTerrain.ToString());

                writer.WriteEndElement();
            }

            writer.WriteEndElement();
        }

        /// <summary>
        /// Determines if the scene contains an object with the same name.
        /// </summary>
        public bool Contains(string name)
        {
            return sceneObjects.Keys.Contains(name);
        }

        /// <summary>
        /// Adds a new object to the scene.
        /// </summary>
        public void Add(string name, BaseObject entity)
        {
            if (Contains(name))
            {
                MessageBox.Show("An entity with this name already exists " +
                                "in the scene, please choose another name");
            }
            else
            {
                entity.Renamed += new RenamedEventHandler(BaseObjectRenamed);
                entity.Renamed += ObjectRenamed;

                sceneObjects.Add(name, entity);
            }
        }
        /// <summary>
        /// Removes an object from the scene.
        /// </summary>
        public void Remove(string name)
        {
            if (Contains(name))
            {
                sceneObjects.Remove(name);
            }
        }

        /// <summary>
        /// Clears all objects from the scene.
        /// </summary>
        public void Clear()
        {
            sceneObjects.Clear();

            heightMap = null;

            heightMapPath = string.Empty;
            colorMapPath = string.Empty;
        }

        /// <summary>
        /// Returns the requested object.
        /// </summary>
        public BaseObject GetBaseObject(string name)
        {
            if (!Contains(name))
            {
                return null;
            }

            return sceneObjects[name];
        }

        /// <summary>
        /// Performs a ray cast to get the closest point to the mouse.
        /// </summary>
        public Vector3? GetClickedPoint(Ray mouseRay)
        {
            float? minValue = null;
            float? result = null;

            if (EngineGlobals.Scene.HeightMap != null)
            {
                result = EngineGlobals.Scene.HeightMap.QuadTree.IntersectsClosest(ref mouseRay);
                minValue = result;
            }

            foreach (BaseObject entity in sceneObjects.Values)
            {
                result = entity.Intersects(mouseRay);

                if (result != null)
                {
                    if (minValue == null)
                    {
                        minValue = float.MaxValue;
                    }
                    if (result < minValue)
                    {
                        minValue = result;
                    }
                }
            }

            if (minValue == null)
            {
                return null;
            }

            return mouseRay.Position + (mouseRay.Direction * minValue);
        }
        /// <summary>
        /// Performs a ray cast to get the closest object to the mouse.
        /// </summary>
        public BaseObject GetClickedBaseObject(Ray mouseRay, string exclude)
        {
            BaseObject selectedNode = null;
            float minValue = float.MaxValue;

            foreach (BaseObject entity in sceneObjects.Values)
            {
                if (entity.Name == exclude)
                {
                    continue;
                }

                float? result = entity.Intersects(mouseRay);

                if (result != null)
                {
                    if (result < minValue)
                    {
                        minValue = (float)result;
                        selectedNode = entity;
                    }
                }
            }

            return selectedNode;
        }

        public List<BaseObject> GetVisibleObjects()
        {
            List<BaseObject> baseObjects = new List<BaseObject>();

            foreach (BaseObject baseObject in sceneObjects.Values)
            {
                if (EngineGlobals.Camera.Frustum.Intersects(baseObject.Bounds))
                {
                    baseObjects.Add(baseObject);
                }
            }

            return baseObjects;
        }

        /// <summary>
        /// Attempts to parse a *.scn file.
        /// </summary>
        public void Load(string path)
        {
            Clear();
                        
            using (XmlTextReader reader = new XmlTextReader(path))
            {
                XmlDocument sceneFile = new XmlDocument();
                sceneFile.Load(reader);

                Load(sceneFile);

                reader.Close();
            }

            EngineGlobals.Scene = this;
        }
        /// <summary>
        /// Attempts to parse a *.scn file.
        /// </summary>
        public void Load(XmlDocument sceneFile)
        {
            Clear();

            foreach (XmlNode node in sceneFile.DocumentElement.ChildNodes)
            {
                if (node.Name == "SceneObjects")
                {
                    foreach (XmlNode entityNode in node.ChildNodes)
                    {
                        XmlAttributeCollection entityAttributes = entityNode.Attributes;
                        string name = entityAttributes["Name"].InnerText;
                        string assetPath = entityAttributes["AssetPath"].InnerText;

                        string groupPath = string.Empty;

                        if (entityAttributes["GroupPath"] != null)
                        {
                            groupPath = entityAttributes["GroupPath"].InnerText;
                        }

                        Vector3 position = EngineGlobals.ParseVector3(entityAttributes["Position"].InnerText);
                        Quaternion rotation = EngineGlobals.ParseQuaternion(entityAttributes["Rotation"].InnerText);
                        Vector3 scale = EngineGlobals.ParseVector3(entityAttributes["Scale"].InnerText);

                        //bool visible = bool.Parse(entityAttributes["Visible"].InnerText);

                        bool placeOnTerrain = true;

                        if (entityAttributes["PlaceOnTerrain"] != null)
                        {
                            placeOnTerrain = bool.Parse(entityAttributes["PlaceOnTerrain"].InnerText);
                        }

                        Model model = null;

                        try
                        {
                            model = EngineGlobals.Content.Load<Model>(assetPath);
                        }

                        catch
                        {
                            MessageBox.Show("An object was not loaded : " + name);
                            continue;
                        }

                        BaseObject entity = new BaseObject(name, model, position, rotation, scale);
                        entity.ModelPath = assetPath;
                        entity.PlaceOnTerrain = placeOnTerrain;
                        entity.GroupPath = groupPath;

                        Add(name, entity);
                    }
                }
                else if (node.Name == "Terrain")
                {
                    XmlAttributeCollection terrainAttributes = node.Attributes;
                    heightMapPath = terrainAttributes["Path"].InnerText;

                    if (terrainAttributes["ColorMapPath"] != null)
                    {
                        colorMapPath = terrainAttributes["ColorMapPath"].InnerText;
                    }

                    float cellSize = float.Parse(terrainAttributes["CellSize"].InnerText,
                                                 CultureInfo.InvariantCulture);

                    if (string.IsNullOrEmpty(heightMapPath))
                    {
                        continue;
                    }

                    Texture2D texture = null;

                    try
                    {
                        texture = EngineGlobals.Content.Load<Texture2D>(heightMapPath);
                    }
                    catch
                    {
                        MessageBox.Show("Height map was not found.");
                        continue;
                    }

                    if (texture == null)
                    {
                        continue;
                    }

                    heightMap = new HeightMap(cellSize);
                    heightMap.LoadHeightMap(texture);

                    float maxHeight = heightMap.MaxHeight;

                    if (terrainAttributes["MaxHeight"] != null)
                    {
                        maxHeight = float.Parse(terrainAttributes["MaxHeight"].InnerText,
                                                CultureInfo.InvariantCulture);
                    }

                    if (string.IsNullOrEmpty(colorMapPath) == false)
                    {
                        Texture2D colorMap = null;

                        try
                        {
                            colorMap = EngineGlobals.Content.Load<Texture2D>(colorMapPath);
                        }
                        catch
                        {
                            MessageBox.Show("Color map not found.");
                        }

                        if (colorMap != null)
                        {
                            heightMap.LoadColormap(colorMap);
                        }
                    }

                    if (MathHelper.Distance(maxHeight, heightMap.MaxHeight) > 0.0001f)
                    {
                        heightMap.UpdateMaxHeight(maxHeight);
                    }

                    int counter = 1;

                    if (node.HasChildNodes)
                    {
                        foreach (XmlNode materialNode in node.ChildNodes[0].ChildNodes)
                        {
                            ReadTerrainMaterial(materialNode, counter);
                            counter += 1;
                        }
                    }
                }
            }

            EngineGlobals.Scene = this;
        }
        /// <summary>
        /// Attempts to write a *.scn file.
        /// </summary>
        public void Save(string path)
        {
            using (XmlTextWriter writer = new XmlTextWriter(path, null))
            {
                writer.Formatting = Formatting.Indented;

                writer.WriteStartElement("Scene");

                if (sceneObjects.Count > 0)
                {
                    WriteBaseObjects(writer);
                }

                if (heightMap != null)
                {
                    writer.WriteStartElement("Terrain");

                    writer.WriteAttributeString("Path", heightMapPath);
                    writer.WriteAttributeString("ColorMapPath", colorMapPath);

                    writer.WriteAttributeString("CellSize", heightMap.CellSize.ToString(
                        CultureInfo.InvariantCulture));

                    writer.WriteAttributeString("MaxHeight", heightMap.MaxHeight.ToString(
                        CultureInfo.InvariantCulture));

                    writer.WriteStartElement("Materials");

                    for (int i = 1; i < 5; i++)
                    {
                        WriteTerrainMaterial(writer, i);
                    }
                    
                    writer.WriteEndElement();

                    writer.WriteEndElement();
                }

                writer.WriteEndElement();

                writer.Close();
            }
        }

        public void WriteTerrainMaterial(XmlTextWriter writer, int index)
        {
            writer.WriteStartElement("Material" + index.ToString());

            writer.WriteStartElement("Path");
            if (heightMap.Materials[index].TextureName != heightMap.Materials[index].Tag)
            {
                writer.WriteValue(heightMap.Materials[index].TextureName);
            }
            writer.WriteEndElement();

            writer.WriteStartElement("Scale");
            writer.WriteValue(heightMap.Materials[index].Scale);
            writer.WriteEndElement();

            writer.WriteEndElement();
        }
        public void ReadTerrainMaterial(XmlNode node, int index)
        {
            foreach (XmlNode child in node.ChildNodes)
            {
                if (child.Name == "Path")
                {
                    heightMap.SetTerrainMaterial(index, EngineGlobals.Content.Load<
                        Texture2D>(Path.ChangeExtension(child.InnerText, null)), child.InnerText);
                }
                else if (child.Name == "Scale")
                {
                    heightMap.SetMaterialScale(index, float.Parse(child.InnerText, CultureInfo.InvariantCulture));
                }
            }
        }

        /// <summary>
        /// Updates all objects in the scene.
        /// </summary>
        public void Update()
        {
            if (heightMap != null)
            {
                heightMap.Update();
            }

            foreach (BaseObject entity in sceneObjects.Values)
            {
                entity.Update();
            }
        }

        /// <summary>
        /// Calls the Draw method of each object.
        /// </summary>
        public void Draw(ICamera camera)
        {
            if (heightMap != null)
            {
                heightMap.Draw(camera.View, camera.Projection);
            }

            EngineGlobals.RenderManager.BeginRendering();
            EngineGlobals.RenderManager.Render();
        }
    }
}
